'''
DocIndexer application shared declarations.
'''
import sys, os, time, fnmatch
if os.name == 'nt': import win32api

#---------------------------------------------------------------------------
# Application globals.
#---------------------------------------------------------------------------

if sys.argv[0]:
    APP_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))
else:
    APP_DIR = '.'

#---------------------------------------------------------------------------
# Application classes.
#---------------------------------------------------------------------------

class EDocIndexer(Exception):
    pass


class Doc(object):
    ''' Wrapper for Lucene document that includes the document ID.
    Behaves like dictionary keyed with document field values.
    '''
    # Indexed document statuses.
    INDEXED = 0         # Indexed OK.
    EMPTY = 1           # Document contains not parseable text.
    NO_PARSER = 2       # No parser available for this document.
    PARSER_ERROR = 3    # The parser threw an error.

    # Index format for date/time fields (ISO 8601).
    DATE_FMT = '%Y-%m-%d %H:%M:%S'

    @classmethod
    def str2time(cls, s):
        return time.mktime(time.strptime(s, cls.DATE_FMT))

    @classmethod
    def time2str(cls, t):
        return time.strftime(cls.DATE_FMT, time.localtime(t))

    def __init__(self, doc, id):
        '''Initialized from Lucene hits collection item at index.'''
        self.values  = {}
        self['id'] = id
        self['fspath'] = None
        for fld in doc.getFields():
            name = fld.name()
            value = doc.get(name)
            if name in ('status','size'):
                value = int(value)
            elif name == 'mtime':
                value = self.str2time(value)
            self[name] = value

    def __getitem__(self, key):
        return self.values[key]

    def __setitem__(self, key, value):
        self.values[key] = value

    def printf(self, fmt):
        ''' Return formatted string representation of document using
            a Python style % interpolation fmt.
        '''
        values = self.values.copy()
        values['mtime'] = self.time2str(values['mtime'])
        return printf(fmt, values)


class Hit(Doc):
    ''' Document plus hit score.
    '''
    def __init__(self, hits, index):
        '''Initialized from Lucene hits collection item at index.'''
        Doc.__init__(self, hits.doc(index), hits.id(index))
        self['score'] = hits.score(index)


class Log:
    ''' Pseudo-singleton class.'''
    stream = None
    quiet = False

    @classmethod
    def close(cls):
        if cls.stream and cls.stream != sys.stdout:
            cls.stream.close()
            cls.stream = None

    @classmethod
    def info(cls,msg):
        if cls.stream and not cls.quiet:
            safe_print(msg, cls.stream)

    @classmethod
    def warning(cls,msg):
        if cls.stream is None or cls.stream == sys.stdout:
            stream = sys.stderr
        else:
            stream = cls.stream
        safe_print('warning: %s' % msg, stream)

    @classmethod
    def error(cls,msg):
        safe_print(msg, sys.stderr)
        if cls.stream and cls.stream not in (sys.stdout, sys.stderr):
            safe_print(msg, cls.stream)


class Conf:
    '''Import executable configuration file to set Conf fields.'''
    import conf
    mime_dict = conf.mime_dict


#---------------------------------------------------------------------------
# Utility functions.
#---------------------------------------------------------------------------

def mime_type(filename):
    '''Return MIME type for file name or empty string if unknown.'''
    ext = os.path.splitext(filename)[1]
    return Conf.mime_dict.get(ext.lower(),'')

def printf(fmt, values):
    ''' Return formatted % interpolation from values dictionary.
    '''
    try:
        return fmt % values
    except:
        raise EDocIndexer, 'illegal format: %s' % fmt

def short_path(path):
    ''' Wrapper for win32 GetShortPathName() API.
        If we're running on win98 GetShortPathNameW() does not exist so we need
        to ensure GetShortPathNameA() is called, we do this by ensuring the
        path is not Unicode.
    '''
    assert os.name == 'nt'
    if sys.getwindowsversion()[3] == 1:     # Check for win98.
        path = str(path)
    return win32api.GetShortPathName(path)

def relpath(dir,path):
    '''Return path relative to dir. If path is not relative to dir return
    the absolute path.'''
    p = os.path.abspath(path)
    if fnmatch.fnmatch(p, dir+'*'):
        p = os.path.normpath('.' + os.path.sep + p[len(dir):])
    return p

def files_gen(dir, includes=['*'], excludes=[]):
    ''' Recursive generator for all files that match one of the includes and
        not one of the excludes.
        includes and excludes are a '|' separated list of file name wildcards.
        Matching is case insensitive.
    '''
    # Local function returns True if filename matches any of the wildcards in
    # patterns.
    def matches(filename, patterns):
        for pattern in patterns:
            if fnmatch.fnmatch(os.path.basename(filename.lower()), pattern.lower()):
                return True
        return False

    assert os.path.isdir(dir), 'is not a directory: %s' % dir
    if matches(dir, excludes):
        return
    # Silently skip directories that can't be accessed.
    try:
        files = os.listdir(dir)
    except:
        return
    for f in files:
        f = os.path.abspath(os.path.join(dir, f))
        if os.path.isdir(f):
            for f in files_gen(f, includes, excludes):
                yield f
        elif os.path.isfile(f):
            if matches(f, includes) and not matches(f, excludes):
                yield f

def shell_escape(s):
    ''' Escape characters that are considered special inside a Bourne Shell
        double-quoted string.'''
    result = s
    result = result.replace('"', r'\"')
    result = result.replace('$', r'\$')
    result = result.replace('`', r'\`')
    return result

import locale
locale.setlocale(locale.LC_ALL, '')

def encode_output(text, errors='strict', file=sys.stdout):
    ''' Encode unicode string for sending to file.
        If text is not unicode return it unchanged.
    '''
    if not isinstance(text, unicode):
        return text
    # Some file like classes (e.g. Stderr) don't have an encoding attribute.
    enc = None
    try:
        enc = file.encoding
    except:
        pass
    if not enc:
        enc = locale.getlocale()[1]
        if not enc:
            enc = 'utf8'
    return text.encode(enc, errors)

def decode_input(text, errors='strict'):
    ''' Decode text to unicode. Try guessing the correct encoding.
        Doesn't fail, replaces unknown characters with Unicode replacement
        character.
    '''
    if not isinstance(text, unicode):
        text = guess_encoding(text, errors)[0]
    return text

def safe_print(s, file=sys.stdout):
    ''' Try to encode output string correctly before printing.
    '''
    print >>file, encode_output(s, 'replace', file)

# Adapted from io.py # in the docutils extension module.
# See http://docutils.sourceforge.net
#     http://www.pyzine.com/Issue008/Section_Articles/article_Encodings.html
#
def guess_encoding(text, errors='strict'):
    ''' Given a byte string, attempt to decode it. Tries the standard UTF-8
        and Latin-1 encodings, Plus several gathered from locale information.
        If successful it returns (decoded_unicode, successful_encoding) If
        unsuccessful it raises a UnicodeError if errors='strict' or decodes
        using UTF-8 and errors.
    '''
    successful_encoding = None
    # Make UTF-8 the first encoding.
    encodings = ['utf8']
    #
    # Add anything we can learn from the locale.
# FIXME: Removed because it throws unsuppressible pychecker
#        warnings on Windows, plus I'm not sure if it's useful.
#    if os.name == 'posix':
#        try:
#            encodings.append(locale.nl_langinfo(locale.CODESET))
#        except AttributeError:
#            pass
    try:
        encodings.append(locale.getlocale()[1])
    except (AttributeError, IndexError):
        pass
    try:
        encodings.append(locale.getdefaultlocale()[1])
    except (AttributeError, IndexError):
        pass
    encodings.append('latin1')  # we try Latin-1 last.
    for enc in encodings:
        if not enc: # Some of the locale calls may have returned None.
            continue
        try:
            decoded = unicode(text, enc)
            successful_encoding = enc
        except (UnicodeError, LookupError):
            pass
        else:
            break
    if not successful_encoding:
        try:
            if errors == 'strict':
                raise UnicodeError
            decoded = unicode(text, 'utf8', errors)
            successful_encoding = 'utf8'
        except (UnicodeError, LookupError):
            raise UnicodeError(
                'unable to decode input data: tried : %s.'
                % ', '.join([repr(enc) for enc in encodings if enc]))
    else:
        return (decoded, successful_encoding)

# vim: ts=4 sw=4:
